package com.marketlive.entity;

/*
(C) Copyright MarketLive. 2006. All rights reserved.
MarketLive is a trademark of MarketLive, Inc.
Warning: This computer program is protected by copyright law and international treaties.
Unauthorized reproduction or distribution of this program, or any portion of it, may result
in severe civil and criminal penalties, and will be prosecuted to the maximum extent
possible under the law.
*/

import org.marketlive.entity.IAttributePage;
import org.marketlive.entity.IEntity;
import org.marketlive.entity.IEntityHome;
import org.marketlive.entity.IPrimaryKey;
import org.marketlive.entity.attributes.IAttribute;
import org.springframework.util.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.io.Serializable;

/**
 * Base class for entities. Entities have an identity and a lifespan that spans more than one process.
 * Each entity is uniquely identified by its primary key.
 */
public abstract class Entity implements IEntity, Serializable {

    /** Logger. */
    private static Log log = LogFactory.getLog(Entity.class);
    /**
     * Primary key.
     */
    private IPrimaryKey pk;
    /**
     * Code.
     */
    private String code;
    /**
     * Hibernate version.
     */
    private int version = -1;
    /**
     * Date entity created.
     */
    private Date dateCreated;
    /**
     * Date entity last modified.
     */
    private Date dateModified;
    
    /**
     * A transient Map of LocalMaps of AttributePages keyed on page name. This map
     * is used to hold newly created collections.  These collection are moved to the persistent map
     * if they contain non-empty AttributePages.
     */
    private Map<String, LocaleMap> transientMapOfLocalizedPages = new HashMap<String, LocaleMap>();
    
    /**
     * A persistent Map of LocalMaps of AttributePages keyed on page name. This map
     * is used by Hibernate.
     */
    private Map<String, LocaleMap> persistentMapOfLocalizedPages = new HashMap<String, LocaleMap>();
    
    /**
     * Dirty flag to control iteration over localized pages.
     */
    private boolean pagesAreDirty = false;
    /**
     * Map of attribute pages keyed on page name, created for non-existing pages.
     */
    private Map<String, IAttributePage>  transientAttributePages = new HashMap<String, IAttributePage>();
    /**
     * Map of attribute pages keyed on page name.
     */
    private Map<String, IAttributePage> persistentAttributePages = new HashMap<String, IAttributePage>();
    
    /**
     * Map of all attributes assigned to this entity.
     */
    private Map attributes;

    /**
     * {@inheritDoc}
     */
    public IPrimaryKey getPk() {
        return pk;
    }

    /**
     * Sets the primary key for this entity.
     * This method is not specified by the interface.
     *
     * @param pk the primary key for this entity
     */
    public void setPk(final IPrimaryKey pk) {
        this.pk = pk;
    }

    /**
     * {@inheritDoc}
     */
    public int getVersion() {
        return version;
    }

    /**
     * Sets the version id. Called by Hibernate. Should not be used by client code.
     * @param version the Hibernate version number for this entity
     */
    public void setVersion(final int version) {
        //todo hide this field?
        this.version = version;
    }

    /**
     * {@inheritDoc}
     */
    public String getCode() {
        return this.code;
    }

    /**
     * {@inheritDoc}
     */
    public void setCode(String pCode) {
        this.code = pCode;
    }

    /**
     * {@inheritDoc}
     */
    public Date getDateCreated() {
        return dateCreated;
    }

    /**
     * {@inheritDoc}
     */
    public void setDateCreated(final Date dateCreated) {
        this.dateCreated = dateCreated;
    }

    /**
     * {@inheritDoc}
     */
    public Date getDateModified() {
        return dateModified;
    }

    /**
     * {@inheritDoc}
     */
    public void setDateModified(final Date dateModified) {
        this.dateModified = dateModified;
    }

    /**
     * Getter for collection of localized attribute pages. Used by Hibernate only.
     * Parses the transient map of LocalizedPages and adds them to the persistent map.
     * @return
     */
    @SuppressWarnings("unused")
    private Map<String, LocaleMap> getLocalizedAttributePages() {

    	if (pagesAreDirty) {
	        for (String pageName : this.transientMapOfLocalizedPages.keySet()) {
	            LocaleMap localeMap = this.transientMapOfLocalizedPages.get(pageName);
	            Map<String,IAttributePage> pages = localeMap.getLocaleAttributePages();
	            if (pages != null) {
	                for (String localeName : pages.keySet()) {
	                    IAttributePage page = pages.get(localeName);
	                    LocaleMap persistentLocaleMap = this.persistentMapOfLocalizedPages.get(pageName);
	                    if (persistentLocaleMap == null) {
	                        persistentLocaleMap = new LocaleMap();
	                        this.persistentMapOfLocalizedPages.put(pageName, persistentLocaleMap);
	                    }
	                    persistentLocaleMap.setPageForLocale(page, StringUtils.parseLocaleString(localeName));
	                }
	            }
	        }
    	}
        
        if (this.persistentMapOfLocalizedPages != null && this.persistentMapOfLocalizedPages.size() == 0) {
            return null;
        } else {
            return this.persistentMapOfLocalizedPages;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, IAttributePage> getAttributePageForAllLocales(String pageName) {
        
    	pagesAreDirty = true;
        HashMap<String, IAttributePage> localizedPages = null;
        
        LocaleMap persistentLocaleMap = this.persistentMapOfLocalizedPages.get(pageName);
        
        if (persistentLocaleMap != null) {
            localizedPages = new HashMap<String, IAttributePage>(persistentLocaleMap.getLocaleAttributePages());
        }
        
        if (localizedPages == null) {
            localizedPages = new HashMap<String, IAttributePage>();
        }
        
        LocaleMap transientLocaleMap = this.transientMapOfLocalizedPages.get(pageName);
        
        if (transientLocaleMap != null) {
            localizedPages.putAll(transientLocaleMap.getLocaleAttributePages());
        }
        
        return localizedPages;
    }
    
    /**
     * Setter for collection of localized attribute pages. Used by Hibernate only.
     * @param localizedAttributePages
     */
    @SuppressWarnings("unused")
    protected void setLocalizedAttributePages(Map<String, LocaleMap> localizedAttributePages) {
    	pagesAreDirty = false;
        if (localizedAttributePages == null) {
            this.persistentMapOfLocalizedPages = new HashMap<String, LocaleMap>();
        }
        else {
            this.persistentMapOfLocalizedPages = localizedAttributePages;
        }
    }

    /**
     * Returns the collection of attribute pages contained in this entity.
     * This is to support mapping attribute pages as dynamic components.
     * For Hibernate use, accessor method may not contain logic.
     *
     * @return a collection of <code>IAttributePages</code> contained in this entity
     */
    private Map<String, IAttributePage> getAttributePages() {

        for (String key : this.transientAttributePages.keySet()) {
            IAttributePage page = this.transientAttributePages.get(key);
            if (!page.isEmpty()) {
                if (this.persistentAttributePages == null) {
                    this.persistentAttributePages = new HashMap<String, IAttributePage>();
                }
                this.persistentAttributePages.put(key, page);
            }
        }

        return this.persistentAttributePages;
    }

    /**
     * Sets the collection of attribute pages contained in this entity. This is to support mapping attribute pages as
     * dynamic components. For Hibernate use, accessor method may not contain logic.
     * @param attributePages
     */
    @SuppressWarnings("unused")
    protected void setAttributePages(Map<String, IAttributePage> attributePages) {

         this.persistentAttributePages = attributePages;
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, IAttributePage> getPages() {
        Map<String, IAttributePage> tempPages = new HashMap<String, IAttributePage>();
        Map<String, IAttributePage> attrPages = this.getAttributePages();
        if (attrPages != null) {
            tempPages.putAll(attrPages);
        }
        return tempPages;
    }

    public Map<String, LocaleMap> getLocalizedPages() {
        Map<String, LocaleMap> tempLocaleMaps = new HashMap<String, LocaleMap>();
        Map<String, LocaleMap> localeMaps = this.getLocalizedAttributePages();
        if (localeMaps != null) {
            tempLocaleMaps.putAll(localeMaps);
        }
        return tempLocaleMaps;
    }

    /**
     * {@inheritDoc}
     */
    public IAttributePage getPage(final String pageName, final Locale locale) {
        
    	pagesAreDirty = true;
        IAttributePage page;
        
        if (getEntityHome().isLocalizedPage(pageName)) {
            
            // Check if a persistent LocaleMap exists for this page type.
            LocaleMap persistentLocaleMap = this.persistentMapOfLocalizedPages.get(pageName);
            
            if (persistentLocaleMap == null) {
                // Check if transient LocaleMap exists for this page type.
                LocaleMap transientLocaleMap = this.transientMapOfLocalizedPages.get(pageName);
                if (transientLocaleMap == null) {
                    // Create new transient LocaleMap
                    transientLocaleMap = new LocaleMap();
                    page = this.getEntityHome().getEmptyPage(pageName);
                    transientLocaleMap.setPageForLocale(page, locale);
                    this.transientMapOfLocalizedPages.put(pageName, transientLocaleMap);
                } else {
                    // Check if transient page exists for given locale
                    page = transientLocaleMap.getPageByLocale(locale);
                    if (page == null) {
                        // Create new transient page
                        page = this.getEntityHome().getEmptyPage(pageName);
                        transientLocaleMap.setPageForLocale(page, locale);
                    }
                }
            } else {
                // Check if persistent page exists for given locale
                page = persistentLocaleMap.getPageByLocale(locale);
                if (page == null) {
                    // Check if transient LocaleMap exists for this page type
                    LocaleMap transientLocaleMap = this.transientMapOfLocalizedPages.get(pageName);
                    if (transientLocaleMap == null) {
                        // Create new transient LocaleMap
                        transientLocaleMap = new LocaleMap();
                        this.transientMapOfLocalizedPages.put(pageName, transientLocaleMap);
                    }
                    page = transientLocaleMap.getPageByLocale(locale);
                    if (page == null) {
                        page = this.getEntityHome().getEmptyPage(pageName);
                        transientLocaleMap.setPageForLocale(page, locale);
                    }
                }
            }
        
        } else {
            
            // Not a localized attribute page 
            if (persistentAttributePages == null 
                    || (page = this.persistentAttributePages.get(pageName)) == null) {
                page = this.transientAttributePages.get(pageName);
                if (page == null) {
                    page = this.getEntityHome().getEmptyPage(pageName);
                    this.transientAttributePages.put(pageName, page);
                }
            }
        }
        
        return page;
    }

    /**
     * Sets the given page on this entity; should be used only for non-localized attribute pages.
     *
     * @param pageName the name of the page to set. If a page with the same name already exists, it is replaced.
     * @param page the <code>IAttributePage</code> to set
     */
    public void setPage(final String pageName, final IAttributePage page) {
        // todo: make sure the page has been defined and is of the right class
        this.transientAttributePages.put(pageName, page);
    }

    /**
     * Returns a cloned Map of this entity's non-localized attribute pages.
     * @return Map  a Map with key = attribute page names and value = IAttributePage
     */
    protected Map<String, IAttributePage> cloneAttributePages() {
        Map<String, IAttributePage> pagesMap = this.getAttributePages();
        Map<String, IAttributePage> clonedPagesMap = new HashMap<String, IAttributePage>();
        if(pagesMap != null) {
            for (String pageName : pagesMap.keySet()) {
                IAttributePage sourcePage = pagesMap.get(pageName);
                if(sourcePage != null) {
                    try {
                        clonedPagesMap.put(pageName, sourcePage.clone());
                    }
                     catch (CloneNotSupportedException e) {
                        log.info("Cloning not supported on page: " + sourcePage.getName());
                    }
                }
            }
        }
        return clonedPagesMap;
    }

    /**
     * Returns a cloned Map of this entity's localized attribute pages.
     * @return Map  a Map of attribute pages
     */
    protected Map<String, LocaleMap> cloneLocalizedAttributePages() {

        Map<String, LocaleMap> localeMaps = this.getLocalizedAttributePages();
        Map<String, LocaleMap> clonedLocaleMaps = new HashMap<String, LocaleMap>();
        if(localeMaps != null) {
            for (String pageName : localeMaps.keySet()) {
                LocaleMap sourceLocaleMap = localeMaps.get(pageName);
                if(sourceLocaleMap != null) {
                    Map<String, IAttributePage> localizedPages = sourceLocaleMap.getLocaleAttributePages();
                    if(localizedPages != null && !localizedPages.isEmpty()) {
                        LocaleMap clonedLocalizedPages = new LocaleMap();
                        for (String localeCode : localizedPages.keySet()) {
                            IAttributePage sourcePage = localizedPages.get(localeCode);
                            if(sourcePage != null) {
                                try {
                                    clonedLocalizedPages.setPageForLocale(sourcePage.clone(),StringUtils.parseLocaleString(localeCode));
                                }
                                catch (CloneNotSupportedException e) {
                                    log.info("Cloning not supported on page: " + sourcePage.getName());
                                }
                            }
                        }
                        clonedLocaleMaps.put(pageName, clonedLocalizedPages);
                    }
                }
            }
        }
        return clonedLocaleMaps;
    }
    
    /**
     * Returns the home object for this entity.
     * @return the Home object responsible for managing this entity
     */
    public abstract IEntityHome getEntityHome();

    /**
     * Returns the type of entity this is, according to the home object that manages it.
     * If this entity's home object is null or if the entity home does not
     * return a value for <code>getEntityType()</code>, the simple name of
     * this class is returned.
     *
     * @see org.marketlive.entity.IEntityHome#getEntityType()
     */
    public String getEntityType() {
        if (getEntityHome() == null || getEntityHome().getEntityType() == null) {
            return this.getClass().getSimpleName();
        }
        return getEntityHome().getEntityType();
    }

    /**
     * Compares two entities using primary key equality.
     * Uses the following algorithm:
     * <ol>
     *   <li>if o is null, return false
     *   <li>if o is not an instance of IEntity, return false
     *   <li>if the entities are of different classes, return false
     *   <li>if both primary keys are null, use instance equality (==)
     *   <li>if only one primary key is null, return false
     *   <li>delegate to this.getPk().equals(o.getPk())
     * </ol>
     * @param o the object to compare
     * @return true if this entity is equal to the given object using primary key equality
     */
    public boolean equals(final Object o) {

        // make sure entity is non-null
        if (o == null) {
            return false;
        }

        if (!(o instanceof IEntity)) {
            return false;
        }

        IEntity entity = (IEntity) o;

        // make sure our entity types are the same
        // may be a better way to do this...
        if (!this.getClass().getName().equals(entity.getClass().getName())) {
            return false;
        }

        IPrimaryKey pk = entity.getPk();

        // if both pks are null, use instance equality.
        if (pk == null && this.getPk() == null) {
            return (this == o);
        }

        // make sure we have non-null pks
        if (pk == null) {
            return false;
        }
        if (this.getPk() == null) {
            return false;
        }

        // compare the pks using IPrimaryKey.equals
        if (!this.getPk().equals(pk)) {
            return false;
        }
        return true;
    }

    /**
     * Returns the hash code of this entity based on its primary key.
     * If the key is null, returns 0 (zero).
     *
     * @return the hash code of this entity's primary key, or 0 if the key is null
     */
    public int hashCode() {
        return (pk != null ? pk.hashCode() : 0);
    }

    /**
     * Returns a description string useful for debugging in the format [entityType:PK].
     * If this entity does not have a home object, entityType is <code>NO-HOME</code>.
     * If this entity does not have a primary key, PK is <code>new</code>.
     *
     * @return a string for use in debugging
     */
    public String toString() {
        if (getEntityHome() == null) {
            return "[NO-HOME:" + getPk() + "]";
        }
        if (getPk() == null) {
            return "[" + getEntityHome().getEntityType() + ":new";
        } else {
            return "[" + getEntityHome().getEntityType() + ":" + getPk() + "]";
        }
    }

    /**
     * {@inheritDoc}
     */
    public IEntity refreshEntity() {
        IEntityHome home = getEntityHome();
        if (home == null) {
            return this;
        } else {
            return this.getEntityHome().findByPk(this.pk);
        }
    }

    /**
     * {@inheritDoc}
     */
    public IEntity refreshEntityLock() {
        IEntityHome home = getEntityHome();
        if (home == null) {
            return this;
        } else {
            return this.getEntityHome().findByPkLock(this.pk);
        }
    }
}
